/*
 * MIT License
 *
 * Copyright (c) 2020 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.openapi.base;

import cn.seaboot.commons.core.CommonUtils;
import cn.seaboot.commons.exception.BizException;
import cn.seaboot.commons.reflect.MethodUtils;
import io.swagger.v3.oas.annotations.Operation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

/**
 * Method 预解析数据
 *
 * @author Mr.css
 * @version 2021-04-01 14:47
 */
public class MethodInfo {
    /**
     * 方法对象
     */
    @NotNull
    private Method method;
    /**
     * 请求方式
     * <p>
     * 如果指定了多种请求方式，我们只会选用其中一种，比如请求方式是 ['POST','GET']，我们只会取第一个；
     * 如果出现这样的代码：if is POST ... else if is GET ...可以尝试将代码分到两个方法中（过于另类的代码不做解析）。
     */
    @NotNull
    private String requestMethod;
    /**
     * 请求 url，所有的 url 都以 '/' 开头，requestPath 的缺省值是 Method.getName();
     */
    @NotNull
    private String[] requestPath;
    /**
     * 方法描述
     */
    @Nullable
    private String methodDescription;
    /**
     * RequestMapping
     */
    @NotNull
    private RequestMapping requestMapping;
    /**
     * 方法参数
     * <p>
     * 其中 parameters.key 值是经过特殊设计的，能准确拿到参数名，{@link Parameter} 不一定能直接获取。
     * <p>
     * Parameter 并不是单纯的 Method.getParameter()，专门指表单的参数，
     * 对于 @SessionAttribute 或其他特殊的注解，直接选择忽略该参数，
     * 相对于项目来说，这些注解使用频率过低，在 Postman 上只需要小小的操作就能弥补。
     */
    @Nullable
    private Map<String, Parameter> parameters;
    /**
     * 函数锁携带的注解，做成成员变量，单纯是为了减少反复调用Java反射
     */
    @Nullable
    private Annotation[] annotations;
    /**
     * 表单类型，如果包含文件的话，需要用 FormData 方式提交
     */
    @NotNull
    private FormType formType = FormType.FORM;

    public MethodInfo(Method method, @NotNull RequestMapping requestMapping) {
        this.method = method;
        this.requestMapping = requestMapping;
        this.annotations = method.getAnnotations();

        // 表单类型
        this.analyzeFormType();
        // url
        this.analyzeRequestPath();
        // 请求方式
        this.analyzeRequestMethod();
        // 表单字段
        this.analyzeRequestParameter();
        // 接口描述
        this.analyzeMethodDescription();
    }

    /**
     * 扫描请求参数。
     * <p>
     * Analyze valid parameter, remove value of session-attribute, request-header, etc.
     */
    private void analyzeRequestParameter() {
        Parameter[] params = method.getParameters();
        String[] names = MethodUtils.getParameterNames(this.method);
        if (names == null || params.length != names.length) {
            throw new BizException("扫描到的参数数量不一致：" + method.getName());
        }
        Map<String, Parameter> ret = new HashMap<>();
        for (int i = 0; i < params.length; i++) {
            Parameter parameter = params[i];
            if (this.isRequestParameter(parameter)) {
                ret.put(names[i], parameter);
            }
        }
        this.parameters = ret;
    }


    /**
     * 确定方法的参数，是不是表单参数，类似于 ModuleAndView 这样的对象，是方法的参数，但是不是表单的参数。
     * <p>
     * Check a request-parameter if valid, method's parameter not only request-parameter,
     * maybe is a value from session or request's header or others,
     * make sure method's parameter is RequestParam or RequestBody.
     * Because @RequestHeader, @SessionAttribute never seem to be used, so ignore them,
     * You can override this method if necessary.
     *
     * @param parameter Parameter
     * @return boolean
     */
    private boolean isRequestParameter(Parameter parameter) {
        RequestParam param = parameter.getDeclaredAnnotation(RequestParam.class);
        if (param != null) {
            return true;
        }
        RequestBody body = parameter.getDeclaredAnnotation(RequestBody.class);
        if (body != null) {
            return true;
        }
        // 包含除了 RequestParam 之外的注释，类似于 Headers 等等，因为使用次数太少，这里直接忽视
        Annotation[] annotations = parameter.getAnnotations();
        if (CommonUtils.isNotEmpty(annotations)) {
            for (Annotation annotation : annotations) {
                if (annotation.getClass().getName().startsWith("org.springframework.web.bind")) {
                    return false;
                }
            }
        }
        // 通常是 ModelAndView 等参数，用于界面渲染的参数
        String parameterType = parameter.getType().getName();
        return !parameterType.startsWith("org.springframework.ui") && !parameterType.startsWith("javax.servlet");
    }

    /**
     * 确认表单类型，普通表单？ FormData？ 还是 RequestBody？
     * <p>
     * There are 3 types of request:
     * simple form, form contain files, request parameter is a json or xml.
     * Different kind of request need different setting.
     */
    private void analyzeFormType() {
        Parameter[] params = method.getParameters();
        for (Parameter parameter : params) {
            if (MultipartFile.class.equals(parameter.getType())) {
                formType = FormType.FORM_DATA;
                break;
            } else {
                RequestBody requestBody = parameter.getAnnotation(RequestBody.class);
                if (requestBody != null) {
                    formType = FormType.BODY;
                    break;
                }
            }
        }
    }

    /**
     * 确定请求方式，多种方式并存，则取第一个。
     * <p>
     * Analyze request-method of method, because request-method 'ALL' is invalid in postman,
     * so, if RequestMethod is undefined, will return 'GET', and postman will use 'GET' request
     * server.
     */
    private void analyzeRequestMethod() {
        RequestMethod[] methods = requestMapping.method();
        if (CommonUtils.isEmpty(methods)) {
            this.requestMethod = "GET";
        } else {
            RequestMethod requestMethod = methods[0];
            this.requestMethod = requestMethod.toString();
        }
    }

    /**
     * 确定请求的 url，缺省值是函数名。
     * <p>
     * Analyze request-path of RequestMapping，
     * if path is undefined, use method's name.
     */
    private void analyzeRequestPath() {
        this.requestPath = requestMapping.value();
        if (CommonUtils.isEmpty(this.requestPath)) {
            this.requestPath = new String[]{'/' + method.getName()};
        } else {
            for (int i = 0; i < this.requestPath.length; i++) {
                String str = this.requestPath[i];
                if (str.charAt(0) != '/') {
                    this.requestPath[i] = '/' + str;
                }
            }
        }
    }

    /**
     * 确定请求的中文描述。
     * <p>
     * Analyze description form method, if {@link Operation#description} is undefined, will use method's name.
     */
    private void analyzeMethodDescription() {
        Operation operation = method.getDeclaredAnnotation(Operation.class);
        if (operation != null) {
            this.methodDescription = operation.description();
        } else {
            this.methodDescription = method.getName();
        }
    }

    //getter/setter-----------------------------------

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public String getRequestMethod() {
        return requestMethod;
    }

    public void setRequestMethod(String requestMethod) {
        this.requestMethod = requestMethod;
    }

    public String[] getRequestPath() {
        return requestPath;
    }

    public void setRequestPath(String[] requestPath) {
        this.requestPath = requestPath;
    }

    public RequestMapping getRequestMapping() {
        return requestMapping;
    }

    public void setRequestMapping(RequestMapping requestMapping) {
        this.requestMapping = requestMapping;
    }

    public Map<String, Parameter> getParameters() {
        return parameters;
    }

    public void setParameters(Map<String, Parameter> parameters) {
        this.parameters = parameters;
    }

    public Annotation[] getAnnotations() {
        return annotations;
    }

    public void setAnnotations(Annotation[] annotations) {
        this.annotations = annotations;
    }

    public FormType getFormType() {
        return formType;
    }

    public void setFormType(FormType formType) {
        this.formType = formType;
    }

    public String getMethodDescription() {
        return methodDescription;
    }

    public void setMethodDescription(String methodDescription) {
        this.methodDescription = methodDescription;
    }
}
